Example 3: Simplest Case - with a Priority Resource

IMPORTANT NOTE!

In vidigi 0.0.5, the original implementation of VidigiPriorityStore was rewritten, with the version available in 0.0.4 renamed VidigiPriorityStoreLegacy.

If upgrading, you can change your import statement from

from vidigi.utils import VidigiPriorityStore

to

from vidigi.resources import VidigiPriorityStoreLegacy as VidigiPriorityStore

to allow your code to continue working in 0.0.5 with no other modifications.

The 0.0.5+ version of VidigiPriorityStore is designed to simplify the incorporation of vidigi into models that use simpy resources, minimizing the syntax changes required when requesting a resource. Take a look at example_7_simplest_case_priority_resource_storewrapper.ex_7_model_classes.py for an example of its use.

from examples.example_3_simplest_case_priority_resource.ex_3_model_classes import Trial, g
from vidigi.prep import reshape_for_animations, generate_animation_df
from vidigi.animation import generate_animation, animate_activity_log
import pandas as pd
import plotly.io as pio
pio.renderers.default = "notebook"
import os
import random
import numpy as np
import pandas as pd
import simpy
from sim_tools.distributions import Exponential, Lognormal
from vidigi.resources import populate_store, VidigiPriorityStoreLegacy as VidigiPriorityStore

class g:
    '''
    Create a scenario to parameterise the simulation model

    Parameters:
    -----------
    random_number_set: int, optional (default=DEFAULT_RNG_SET)
        Set to control the initial seeds of each stream of pseudo
        random numbers used in the model.

    n_cubicles: int
        The number of treatment cubicles

    trauma_treat_mean: float
        Mean of the trauma cubicle treatment distribution (Lognormal)

    trauma_treat_var: float
        Variance of the trauma cubicle treatment distribution (Lognormal)

        arrival_rate: float
        Set the mean of the exponential distribution that is used to sample the
        inter-arrival time of patients

    '''
    random_number_set = 42

    n_cubicles = 4
    trauma_treat_mean = 40
    trauma_treat_var = 5

    arrival_rate = 5
    sim_duration = 600
    number_of_runs = 100

class Patient:
    '''
    Class defining details for a patient entity
    '''
    def __init__(self, p_id):
        '''
        Constructor method

        Params:
        -----
        identifier: int
            a numeric identifier for the patient.
        '''
        self.identifier = p_id
        self.arrival = -np.inf
        self.wait_treat = -np.inf
        self.total_time = -np.inf
        self.treat_duration = -np.inf

        # Randomly initialise a patient priority value
        # Lower values will be prioritised - so priority 1 will be seen before priority 2
        if random.uniform(0, 1) < 0.2:
            self.priority = 1
        else:
            self.priority = 2

class Model:
    '''
    Simulates the simplest minor treatment process for a patient

    1. Arrive
    2. Examined/treated by nurse when one available
    3. Discharged
    '''
    # Constructor to set up the model for a run.  We pass in a run number when
    # we create a new model.
    def __init__(self, run_number):
        # Create a SimPy environment in which everything will live
        self.env = simpy.Environment()

        self.event_log = []

        # Create a patient counter (which we'll use as a patient ID)
        self.patient_counter = 0

        self.patients = []

        # Create our resources
        self.init_resources()

        # Store the passed in run number
        self.run_number = run_number

        # Create a new Pandas DataFrame that will store some results against
        # the patient ID (which we'll use as the index).
        self.results_df = pd.DataFrame()
        self.results_df["Patient ID"] = [1]
        self.results_df["Queue Time Cubicle"] = [0.0]
        self.results_df["Time with Nurse"] = [0.0]
        self.results_df.set_index("Patient ID", inplace=True)

        # Create an attribute to store the mean queuing times across this run of
        # the model
        self.mean_q_time_cubicle = 0

        self.patient_inter_arrival_dist = Exponential(mean = g.arrival_rate,
                                                      random_seed = self.run_number*g.random_number_set)
        self.treat_dist = Lognormal(mean = g.trauma_treat_mean,
                                    stdev = g.trauma_treat_var,
                                    random_seed = self.run_number*g.random_number_set)

    def init_resources(self):
        '''
        Init the number of resources
        and store in the arguments container object

        Resource list:
            1. Nurses/treatment bays (same thing in this model)

        '''
        self.treatment_cubicles = VidigiPriorityStore(self.env)

        populate_store(num_resources=g.n_cubicles,
                       simpy_store=self.treatment_cubicles,
                       sim_env=self.env)

    # A generator function that represents the DES generator for patient arrivals
    def generator_patient_arrivals(self):
        # Use an infinite loop here to keep doing this indefinitely while the simulation runs
        while True:
            # Increment the patient counter by 1 (first patient will have an ID of 1)
            self.patient_counter += 1

            p = Patient(self.patient_counter)

            # Store patient in list for later easy access
            self.patients.append(p)

            # Tell SimPy to start up the attend_clinic generator function with this patient
            # (the generator function that will model the patient's journey through the system)
            self.env.process(self.attend_clinic(p))

            # Randomly sample the time to the next patient arriving
            sampled_inter = self.patient_inter_arrival_dist.sample()

            # Freeze this instance of this function in place until the inter-arrival time
            # sampled above has elapsed
            yield self.env.timeout(sampled_inter)

    def attend_clinic(self, patient):
        """
        A generator function that represents the pathway for a patient going through the clinic.

        The patient object is passed in to the generator function so we can extract information
        from / record information to it
        """
        self.arrival = self.env.now

        # ===== LOGGING FOR VIDIGI ANIMATION  ===== #
        self.event_log.append(
            {'patient': patient.identifier,
             'pathway': patient.priority,
             'event_type': 'arrival_departure',
             'event': 'arrival',
             'time': self.env.now}
        )
        # ========================================= #

        # request examination resource
        start_wait = self.env.now

        # ===== LOGGING FOR VIDIGI ANIMATION  ===== #
        self.event_log.append(
            {'patient': patient.identifier,
             'pathway': patient.priority,
             'event': 'treatment_wait_begins',
             'event_type': 'queue',
             'time': self.env.now}
        )
        # ========================================= #

        # Seize a treatment resource when available
        # Note that we must pass in the patient priority
        treatment_resource = yield self.treatment_cubicles.get(priority=patient.priority)

        # record the waiting time for registration
        self.wait_treat = self.env.now - start_wait

        # ===== LOGGING FOR VIDIGI ANIMATION  ===== #
        self.event_log.append(
            {'patient': patient.identifier,
                'pathway': patient.priority,
                'event': 'treatment_begins',
                'event_type': 'resource_use',
                'time': self.env.now,
                'resource_id': treatment_resource.id_attribute
                }
        )
        # ========================================= #

        # sample treatment duration
        self.treat_duration = self.treat_dist.sample()
        yield self.env.timeout(self.treat_duration)

        # ===== LOGGING FOR VIDIGI ANIMATION  ===== #
        self.event_log.append(
            {'patient': patient.identifier,
                'pathway': patient.priority,
                'event': 'treatment_complete',
                'event_type': 'resource_use_end',
                'time': self.env.now,
                'resource_id': treatment_resource.id_attribute}
        )
        # ========================================= #

        # Resource is no longer in use, so put it back in
        self.treatment_cubicles.put(treatment_resource)

        # total time in system
        self.total_time = self.env.now - self.arrival

        # ===== LOGGING FOR VIDIGI ANIMATION  ===== #
        self.event_log.append(
            {'patient': patient.identifier,
            'pathway': patient.priority,
            'event': 'depart',
            'event_type': 'arrival_departure',
            'time': self.env.now}
        )
        # ========================================= #


    # This method calculates results over a single run.  Here we just calculate
    # a mean, but in real world models you'd probably want to calculate more.
    def calculate_run_results(self):
        # Take the mean of the queuing times across patients in this run of the
        # model.
        self.mean_q_time_cubicle = self.results_df["Queue Time Cubicle"].mean()

    # The run method starts up the DES entity generators, runs the simulation,
    # and in turns calls anything we need to generate results for the run
    def run(self):
        # Start up our DES entity generators that create new patients.  We've
        # only got one in this model, but we'd need to do this for each one if
        # we had multiple generators.
        self.env.process(self.generator_patient_arrivals())

        # Run the model for the duration specified in g class
        self.env.run(until=g.sim_duration)

        # Now the simulation run has finished, call the method that calculates
        # run results
        self.calculate_run_results()

        self.event_log = pd.DataFrame(self.event_log)

        self.event_log["run"] = self.run_number

        return {'results': self.results_df, 'event_log': self.event_log}

# Class representing a Trial for our simulation - a batch of simulation runs.
class Trial:
    # The constructor sets up a pandas dataframe that will store the key
    # results from each run against run number, with run number as the index.
    def  __init__(self):
        self.df_trial_results = pd.DataFrame()
        self.df_trial_results["Run Number"] = [0]
        self.df_trial_results["Arrivals"] = [0]
        self.df_trial_results["Mean Queue Time Cubicle"] = [0.0]
        self.df_trial_results.set_index("Run Number", inplace=True)

        self.all_event_logs = []

    # Method to run a trial
    def run_trial(self):
        print(f"{g.n_cubicles} nurses")
        print("") ## Print a blank line

        # Run the simulation for the number of runs specified in g class.
        # For each run, we create a new instance of the Model class and call its
        # run method, which sets everything else in motion.  Once the run has
        # completed, we grab out the stored run results (just mean queuing time
        # here) and store it against the run number in the trial results
        # dataframe.
        for run in range(g.number_of_runs):
            random.seed(run)

            my_model = Model(run)
            model_outputs = my_model.run()
            patient_level_results = model_outputs["results"]
            event_log = model_outputs["event_log"]

            self.df_trial_results.loc[run] = [
                len(patient_level_results),
                my_model.mean_q_time_cubicle,
            ]

            # print(event_log)

            self.all_event_logs.append(event_log)

        self.all_event_logs = pd.concat(self.all_event_logs)
my_trial = Trial()

my_trial.run_trial()
4 nurses
my_trial.all_event_logs.head(50)
patient pathway event_type event time resource_id run
0 1 2 arrival_departure arrival 0.000000 NaN 0
1 1 2 queue treatment_wait_begins 0.000000 NaN 0
2 1 2 resource_use treatment_begins 0.000000 1.0 0
3 2 2 arrival_departure arrival 3.399660 NaN 0
4 2 2 queue treatment_wait_begins 3.399660 NaN 0
5 2 2 resource_use treatment_begins 3.399660 2.0 0
6 3 2 arrival_departure arrival 8.497645 NaN 0
7 3 2 queue treatment_wait_begins 8.497645 NaN 0
8 3 2 resource_use treatment_begins 8.497645 3.0 0
9 4 2 arrival_departure arrival 8.596678 NaN 0
10 4 2 queue treatment_wait_begins 8.596678 NaN 0
11 4 2 resource_use treatment_begins 8.596678 4.0 0
12 5 2 arrival_departure arrival 8.608025 NaN 0
13 5 2 queue treatment_wait_begins 8.608025 NaN 0
14 6 2 arrival_departure arrival 11.359739 NaN 0
15 6 2 queue treatment_wait_begins 11.359739 NaN 0
16 7 2 arrival_departure arrival 19.509442 NaN 0
17 7 2 queue treatment_wait_begins 19.509442 NaN 0
18 8 2 arrival_departure arrival 22.877356 NaN 0
19 8 2 queue treatment_wait_begins 22.877356 NaN 0
20 9 2 arrival_departure arrival 26.653863 NaN 0
21 9 2 queue treatment_wait_begins 26.653863 NaN 0
22 1 2 resource_use_end treatment_complete 40.317385 1.0 0
23 1 2 arrival_departure depart 40.317385 NaN 0
24 5 2 resource_use treatment_begins 40.317385 1.0 0
25 10 2 arrival_departure arrival 40.737793 NaN 0
26 10 2 queue treatment_wait_begins 40.737793 NaN 0
27 2 2 resource_use_end treatment_complete 42.443230 2.0 0
28 2 2 arrival_departure depart 42.443230 NaN 0
29 6 2 resource_use treatment_begins 42.443230 2.0 0
30 4 2 resource_use_end treatment_complete 48.809628 4.0 0
31 4 2 arrival_departure depart 48.809628 NaN 0
32 7 2 resource_use treatment_begins 48.809628 4.0 0
33 3 2 resource_use_end treatment_complete 51.483457 3.0 0
34 3 2 arrival_departure depart 51.483457 NaN 0
35 8 2 resource_use treatment_begins 51.483457 3.0 0
36 11 2 arrival_departure arrival 71.026558 NaN 0
37 11 2 queue treatment_wait_begins 71.026558 NaN 0
38 5 2 resource_use_end treatment_complete 77.447488 1.0 0
39 5 2 arrival_departure depart 77.447488 NaN 0
40 9 2 resource_use treatment_begins 77.447488 1.0 0
41 6 2 resource_use_end treatment_complete 83.962251 2.0 0
42 6 2 arrival_departure depart 83.962251 NaN 0
43 10 2 resource_use treatment_begins 83.962251 2.0 0
44 12 2 arrival_departure arrival 87.458700 NaN 0
45 12 2 queue treatment_wait_begins 87.458700 NaN 0
46 13 2 arrival_departure arrival 87.465138 NaN 0
47 13 2 queue treatment_wait_begins 87.465138 NaN 0
48 7 2 resource_use_end treatment_complete 95.498040 4.0 0
49 7 2 arrival_departure depart 95.498040 NaN 0
STEP_SNAPSHOT_MAX = 45
LIMIT_DURATION = g.sim_duration
WRAP_QUEUES_AT = 15
full_patient_df = reshape_for_animations(
    event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
    entity_col_name="patient",
    every_x_time_units=2,
    step_snapshot_max=STEP_SNAPSHOT_MAX,
    limit_duration=LIMIT_DURATION,
    debug_mode=True
    )

full_patient_df.head(15)
Iteration through time-unit-by-time-unit logs complete 12:14:03
Snapshot df concatenation complete at 12:14:03
index patient pathway event_type event time resource_id run rank snapshot_time additional
0 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 0 NaN
1 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 2 NaN
2 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 4 NaN
3 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 6 NaN
4 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 8 NaN
5 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 10 NaN
6 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 12 NaN
7 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 14 NaN
8 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 16 NaN
9 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 18 NaN
10 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 20 NaN
11 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 22 NaN
12 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 24 NaN
13 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 26 NaN
14 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 28 NaN
event_position_df = pd.DataFrame([
                    {'event': 'arrival',
                     'x':  50, 'y': 300,
                     'label': "Arrival" },

                    # Triage - minor and trauma
                    {'event': 'treatment_wait_begins',
                     'x':  205, 'y': 275,
                     'label': "Waiting for Treatment"},

                    {'event': 'treatment_begins',
                     'x':  205, 'y': 175,
                     'resource':'n_cubicles',
                     'label': "Being Treated"},

                    {'event': 'exit',
                     'x':  270, 'y': 70,
                     'label': "Exit"}

                ])

Generate animation using the step-by-step functions

Using the three step-by-step functions allows us to intervene in the produced dataframe and manually take control of the icons in use.

This will allow us to show the high-priority patients with a unique icon so we can see their frequency and how they are handled in the final model.

full_patient_df_plus_pos = generate_animation_df(
    full_entity_df=full_patient_df,
    event_position_df=event_position_df,
    entity_col_name="patient",
    wrap_queues_at=WRAP_QUEUES_AT,
    step_snapshot_max=STEP_SNAPSHOT_MAX,
    gap_between_entities=10,
    gap_between_resources=10,
    gap_between_queue_rows=30,
    debug_mode=True
    )

full_patient_df_plus_pos.sort_values(['patient', 'snapshot_time']).head(15)
Placement dataframe finished construction at 12:14:29
index patient pathway event_type event time resource_id run rank snapshot_time additional x y_final label resource x_final row icon
10186 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 0 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10187 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 2 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10188 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 4 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10189 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 6 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10190 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 8 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10191 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 10 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10192 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 12 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10193 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 14 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10194 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 16 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10195 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 18 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10196 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 20 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10197 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 22 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10198 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 24 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10199 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 26 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
10200 2 1 1 resource_use treatment_begins 0.0 1.0 1 1.0 28 NaN 205 175.0 Being Treated n_cubicles 205.0 0.0 🧔🏼
def show_priority_icon(row):
            if "more" not in row["icon"]:
                if row["pathway"] == 1:
                        return "🚨"
                else:
                    return row["icon"]
            else:
                return row["icon"]
full_patient_df_plus_pos = full_patient_df_plus_pos.assign(
            icon=full_patient_df_plus_pos.apply(show_priority_icon, axis=1)
            )
full_patient_df_plus_pos.head(15)
index patient pathway event_type event time resource_id run rank snapshot_time additional x y_final label resource x_final row icon
0 155 49 2 queue exit 208.540636 NaN 1 1.0 598 NaN 270 70.0 Exit NaN 270.0 0.0 🧕🏾
1 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 208 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
2 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 210 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
3 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 212 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
4 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 214 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
5 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 216 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
6 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 218 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
7 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 220 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
8 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 222 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
9 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 224 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
10 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 226 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
11 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 228 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
12 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 230 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
13 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 27.0 232 NaN 205 305.0 Waiting for Treatment NaN 95.0 1.0 🧕🏾
14 155 49 2 queue treatment_wait_begins 206.540636 NaN 1 26.0 234 NaN 205 305.0 Waiting for Treatment NaN 105.0 1.0 🧕🏾
generate_animation(
        full_entity_df_plus_pos=full_patient_df_plus_pos.sort_values(['patient', 'snapshot_time']),
        event_position_df= event_position_df,
        scenario=g(),
        entity_col_name="patient",
        debug_mode=True,
        setup_mode=False,
        include_play_button=True,
        entity_icon_size=20,
        plotly_height=700,
        frame_duration=800,
        frame_transition_duration=200,
        plotly_width=1200,
        override_x_max=300,
        override_y_max=500,
        time_display_units="dhm",
        display_stage_labels=False,
        add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
    )
Output animation generation complete at 12:14:56

Rerun, but using the all-in-one animation function (which will not show different priority icons)

animate_activity_log(
        event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
        event_position_df= event_position_df,
        scenario=g(),
        entity_col_name="patient",
        debug_mode=True,
        setup_mode=False,
        every_x_time_units=1,
        include_play_button=True,
        entity_icon_size=20,
        gap_between_entities=6,
        gap_between_queue_rows=25,
        plotly_height=700,
        frame_duration=200,
        plotly_width=1200,
        override_x_max=300,
        override_y_max=500,
        limit_duration=g.sim_duration,
        wrap_queues_at=25,
        step_snapshot_max=125,
        time_display_units="dhm",
        display_stage_labels=False,
        add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
    )
Animation function called at 12:14:57
Iteration through time-unit-by-time-unit logs complete 12:15:00
Snapshot df concatenation complete at 12:15:00
Reshaped animation dataframe finished construction at 12:15:00
Placement dataframe finished construction at 12:15:00
Output animation generation complete at 12:15:03
Total Time Elapsed: 6.22 seconds